package com.virtual.repository.common.managers.concurrent;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import com.virtual.repository.common.criteria.Criterion;
import com.virtual.repository.common.managers.QueryExecutionData;
import com.virtual.repository.common.managers.QueryExecutionResultData;
import com.virtual.repository.common.managers.QueryManager;
import com.virtual.repository.common.model.Mapping;
import com.virtual.repository.common.model.ModelConfiguration;
import com.virtual.repository.common.model.Property;
import com.virtual.repository.common.operators.CriteriaOperator;
import com.virtual.repository.common.operators.LogicalOperator;
import com.virtual.repository.common.queries.Query;
import com.virtual.repository.common.queries.ResultEntity;
import com.virtual.repository.plugins.api.IDatabasePlugin;


public class ConcurrentQueryManager extends QueryManager {

	private ExecutorService executor;
	
	@Override
	public void initalize(ModelConfiguration modelConfig, Map<String, IDatabasePlugin> plugins) {
		super.initalize(modelConfig, plugins);
		this.executor = Executors.newCachedThreadPool();
	}
	
	
	@Override
	protected List<QueryExecutionResultData> executeSubqueries(QueryExecutionData executionData) throws Exception {
		List<QueryExecutionResultData> results = new ArrayList<>();
		
		// just execute the final query
		Collection<ResultEntity> subResult = executionData.getExecutionPlugin().executeQuery(executionData.getExecutionQuery());

		// check if type of the result is consistent with declaration, if no,
		// then throw exception and terminate selection
		if (executionData.isCollectionResult()) {
			for (ResultEntity singleEntity : subResult) {
				QueryExecutionResultData resultData = new QueryExecutionResultData(executionData, singleEntity);
				results.add(resultData);
			}
		} else {
			if (subResult != null && subResult.size() > 1) {
				throw new Exception("Query: " + executionData.getExecutionQuery() + " returned too many records! [expected: 1, returned: " + subResult.size() + "]");
			}

			if (subResult != null && !subResult.isEmpty()) {
				QueryExecutionResultData resultData = new QueryExecutionResultData(executionData, subResult.toArray(new ResultEntity[0])[0]);
				results.add(resultData);
			}
		}
		
		if (executionData.hasSubqueries()) {//prepare and execute sub-queries
			//first, call query to get identifiers of root entity
			Mapping parentMapping = this.modelConfig.getMapping(executionData.getExpectedResultClass().getName());
			Property identificationProp = parentMapping.getIdentifierProperty();
			String identificationFieldName = identificationProp.getSourceName();
			Query identificationQuery = executionData.getIdentificationQuery(identificationFieldName);
			
			//get identifiers and check if the returned number is consistent with the type of the query
			Collection<ResultEntity> resultIdentifiers = executionData.getExecutionPlugin().executeQuery(identificationQuery);
			if (resultIdentifiers != null && resultIdentifiers.size() > 1 && !executionData.isCollectionResult()) {
				throw new Exception("Identification query: " + identificationQuery + " returned too many results [expected: 1, returned: " + resultIdentifiers.size() + "]");
			}
				
			//next, for each identifier execute all sub-queries and set appropriate foreign key condition 
			for (int idnNum = 0; idnNum < resultIdentifiers.size(); idnNum++) {
				QueryExecutionResultData resultDataTmp = results.get(idnNum);
				Object idnValue = resultIdentifiers.toArray(new ResultEntity[0])[idnNum].getField(identificationProp.getSourceName());
				
				List<Future<List<QueryExecutionResultData>>> futureReturns = new ArrayList<>(executionData.getSubExecutionData().size());
				
				for (QueryExecutionData subQueryExecData : executionData.getSubExecutionData().values()) {
					//prepare foreign key name
					Mapping subQueryMapping = this.modelConfig.getMapping(subQueryExecData.getExpectedResultClass().getName());
					String foreignKey = parentMapping.getId() + "." + identificationProp.getName();
					Property foreignKeyProp = subQueryMapping.getForeignKeys().get(foreignKey);
					if (foreignKeyProp == null) {
						System.out.println("Foreign key name for property: " + subQueryExecData.getExpectedResultClass() + " not found!");
						continue;
					}
					
					//remember original criteria
					List<Criterion> originalCritria = new ArrayList<>(subQueryExecData.getExecutionQuery().getSelectionCriteria());
					
					//add criterion for foreign key
					Criterion criterion = null;
					if (subQueryExecData.getExecutionQuery().getSelectionCriteria() != null && !subQueryExecData.getExecutionQuery().getSelectionCriteria().isEmpty()) {
						criterion = new Criterion(LogicalOperator.AND, foreignKeyProp.getSourceName(), CriteriaOperator.EQUALS, idnValue);
					} else {
						criterion = new Criterion(foreignKeyProp.getSourceName(), CriteriaOperator.EQUALS, idnValue);
					}
					subQueryExecData.getExecutionQuery().getSelectionCriteria().add(criterion);
					
					//recursive invocation - concurrent
					Future<List<QueryExecutionResultData>> futureSubQueryResults = this.executor.submit(new SubqueriesExecutor(subQueryExecData, originalCritria));
					futureReturns.add(futureSubQueryResults);
				}
				
				for (Future<List<QueryExecutionResultData>> futureReturn : futureReturns) {
					resultDataTmp.getSubQueryResults().addAll(futureReturn.get());
				}
			}
		} 
		
		return results;
	}
	
	private class SubqueriesExecutor implements Callable<List<QueryExecutionResultData>> {

		private QueryExecutionData executionData;
		private List<Criterion> originalCritria;
		
		public SubqueriesExecutor(QueryExecutionData executionData, List<Criterion> originalCritria) {
			this.executionData = executionData;
			this.originalCritria = originalCritria;
		}
		
		@Override
		public List<QueryExecutionResultData> call() throws Exception {
			List<QueryExecutionResultData> result = executeSubqueries(executionData);
			//restore original criteria
			executionData.getExecutionQuery().setSelectionCriteria(originalCritria);
			
			return result;
		}
		
	}
}
